package com.binc.testspring.study.mina.study2;

import lombok.Data;
import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.AttributeKey;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.ProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;

/**
 * FileName: MyProtocolDecoder  自定义协议的解码器
 * Autho: binC
 * Date:  2022/3/25 15:27
 */
// public class MyProtocolDecoder extends ProtocolDecoderAdapter {  按理说的话我们继承这个会更好
public class MyProtocolDecoder implements ProtocolDecoder {

    private final AttributeKey  CONTEXT = new AttributeKey(this.getClass(),"context");// // binc_read 这是个哈，有待观察

    private final Charset charset; //字符集
    private int maxPackageLength = 100; //自定义协议包最大长度100   如果超长了，就报异常

    public MyProtocolDecoder() {
        this(Charset.defaultCharset()); //这里是给个默认值
    }
    public MyProtocolDecoder(Charset charset) {
        this.charset = charset;
    }

    // getter setter 方法
    public MyProtocolDecoder setMaxPackageLength(int maxPackageLength) {
        if (maxPackageLength < 0) {
            throw new IllegalArgumentException("maxPackageLength参数异常");
        }
        this.maxPackageLength = maxPackageLength;
        return this;
    }
    public int getMaxPackageLength() {
        return maxPackageLength;
    }

    public Context getContext(IoSession session){
        Context cxt = (Context)session.getAttribute(CONTEXT);
        if (cxt == null) {
            cxt = new Context();
            session.setAttribute(CONTEXT,cxt);
        }
        return cxt;
    }



    @Override
    public void decode(IoSession ioSession, IoBuffer ioBuffer, ProtocolDecoderOutput output) throws Exception {
        final  int packageHeadLength = 5; //定义一个 包头长度  （我们自定一的协议包长度是 int+byte 等于5字节）   不同的协议规则，这里的值自行修改
        //获取自定义的上下文
        Context ctx = getContext(ioSession);

        // ioBuffer.setAutoExpand(true);
        // 将iobuffer 追加到我们的自定义的上下文中
        ctx.appendBuffer(ioBuffer);  // 相当于半包的时候，多次读取进行拼接，然后在处理整包内容
        // 获取追加后的buffer
        IoBuffer buffer = ctx.getIoBuffer();
        buffer.flip(); // 复位，将limit 置成position  ；然后position置成0
        while (buffer.remaining() > packageHeadLength) {
            //ByteBuffer.remaining()查看该ByteBuffer当前的剩余可用长度。   position 到limit位置的长度
            // 如果能读取到的buffer的长度大于5字节了了，说明可以读取到完整的包头的信息了（int 4 + byte1）
            //标记一下
            buffer.mark();
            //读取自定义协议包的
            int headLength = buffer.getInt();
            byte version = buffer.get();
            if (headLength < 0 || headLength > maxPackageLength) {
                // 这个maxPackageLength属于整体包头+内容的长度限定；解码类的限定，我们的自定义协议类没有这样的限定
                // 这个时候的数据是无效的，我们重置
                buffer.reset(); // 这个reset 依赖前边执行的mark（）；
                break;
            } else if (headLength >= packageHeadLength && buffer.remaining() >= (headLength - packageHeadLength)) {
                //这才是最正常的时候    【当前iobuffer的长度大于自定义协议包包头长度，iobuffer剩余未读取的长度 大于等于 自定义协议包的内容的长度的话 】   即：这里是整包和粘包的情况，我们从这里读取我们需要的部分，不是的不要读取，留给下次读取
                //读取一下limit的位置shi
                int oldLimit = buffer.limit();
                //重新设置limit的位置，如果当前buffer中的内容由粘包的话，不会读取到下一个报的内容  上边执行过 buffer.getInt() 和 buffer.get() 了,所以position这时候的的位置是在第6好位置，所以这里要减去已读取的5字节
                buffer.limit(buffer.position() + (headLength - packageHeadLength)); // 设置之后，从position 到limit 的内容就是我们自定义协议包中的content的内容了，limit到oldlimit的内容是粘包后的内容；不关心了    我们自己的西医规则是 HeadLength的大小是 HeadLength+version+content的所有内容的字节长度；如果不是的话，这里的规则要相应的比变化
                String content = buffer.getString(ctx.getDecoder());
                buffer.limit(oldLimit);// 这里将limit没有读取的内容，会在下次读取的时候处理的；本次解码不用关系了；
                //转换成我们自己定义的协议包对象
                MyProtocolPackage myProtocolPackage = new MyProtocolPackage(version, content); // 这样就完成了从字节流向我们自定义的协议包对象的转换
                output.write(myProtocolPackage);//写出去   交给handler去处理

            } else { //半包
                //这个分支是，iobuffer中的内容并不是完整的内容， 属于半包情况，我们先不处理，直接放到上线问中，在从字节流中读取一内容，直到iobuffer中包含了完整的信息
//                buffer.clear(); // 把position设为0，把limit设为capacity，一般在把数据写入Buffer前调用。 mark =-1
                buffer.reset(); // reset 将position的之设置到mark的位置； 随后继续从字节流中读取数据并且将独到的东西放到buffer中，常识判断这个bufer是否还是半包的状态，如果是的话如此循环直到读取到所有的信息；如果不是半包甚至是粘包的话，下次就不会进入的这个分支了，直接开始截取有用的信息进行新的解码操作了

                break;
            }
        }

        if (buffer.hasRemaining()) { //半包 粘包 都走这里
            // buffer中还有未读取的数据的话，需要将这些数据放到buffer中buffer的前边去 【即将粘包多出来的数据   或者  半包未读取的数据 重新放到buffer中】
            IoBuffer temp = IoBuffer.allocate(maxPackageLength).setAutoExpand(true);
            temp.put(buffer);
            temp.flip();
            buffer.reset();
            buffer.put(temp);

            //搞得这么复杂，感觉可以用compact  上述的好处是，position与limit直相同，如果用compact的话，极端情况可能会出现limit大于postion的情况，出现一对空位就不好了；
        } else {
            buffer.reset();
        }




    }

    @Override
    public void finishDecode(IoSession ioSession, ProtocolDecoderOutput protocolDecoderOutput) throws Exception {

    }

    @Override
    public void dispose(IoSession ioSession) throws Exception {
        Context cxt = (Context)ioSession.getAttribute(CONTEXT);
        if (cxt !=null) {
            ioSession.removeAttribute(CONTEXT);
        }
    }

    /**
     * 创建一个内部类   即：自定义的上线问；存放更多的iobuffer  （存放更多的信息）
     */
    @Data
    private class Context{
        private final CharsetDecoder decoder;
        private IoBuffer ioBuffer;

        public Context() {
            this.decoder = charset.newDecoder();
            this.ioBuffer = IoBuffer.allocate(0).setAutoExpand(true);
        }

        /**
         * 写一个追加iobuffer的方法
         */
        public void appendBuffer(IoBuffer ioBuffer){
            this.getIoBuffer().put(ioBuffer);
        }

        public  void reset(){
            decoder.reset();
        }

    }

}
